using System;
using System.Web;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;

namespace Moxiecode.TinyMCE.Compression 
{
    enum CloseMode { None, InStream, OutStream, Both }
    enum CompressItemType { File, Chunk }

    struct CompressItem
    {
        public string Data { get; set; }
        public CompressItemType Type { get; set; }
    }

	public class GzipCompressor 
    {
		public GzipCompressor() { }

        private List<CompressItem> items = new List<CompressItem>();
        public bool NoCompression { get; set; }
        public bool DiskCache { get; set; }
        public string CachePath { get; set; }

		public void AddFile(string path) 
        {
			if (File.Exists(path)) items.Add(new CompressItem() { Type = CompressItemType.File, Data = path });
            else throw new FileNotFoundException("File \"" + path + "\"could not be found, unable to add it for Gzip compression.", path);
		}

		public void AddData(string data) 
        {
			items.Add(new CompressItem() { Type = CompressItemType.Chunk, Data = data });
		}

		public void Compress(Stream to_stream) 
        {
			GZipStream gzipStream = null;
			string key = "";
			byte[] bytes;

			// Build cache key
            items.FindAll(i => i.Type == CompressItemType.File).ForEach(i => key += i.Data);
			key = MD5(key);

			if (this.NoCompression) 
            {
				this.SendPlainText(key, to_stream);
				return;
			}

			// Check for cached file on disk, stream that one if it was found
			if (this.DiskCache && File.Exists(Path.Combine(this.CachePath, key + ".gz"))) 
            {
				this.StreamFromTo(File.OpenRead(Path.Combine(this.CachePath, key + ".gz")), to_stream, 4096, CloseMode.InStream);
				return;
			}

			try 
            {
				// Build new gzipped stream
				if (this.DiskCache) 
                {
				    gzipStream = new GZipStream(File.OpenWrite(Path.Combine(this.CachePath, key + ".gz")), CompressionMode.Compress);

					// Compress all files into memory
					foreach (CompressItem item in items) 
                    {
						// Add file
						if (item.Type == CompressItemType.File) 
                            StreamFromTo(File.OpenRead(item.Data), gzipStream, 4096, CloseMode.InStream);

						// Add chunk
						if (item.Type == CompressItemType.Chunk) 
                        {
							bytes = Encoding.ASCII.GetBytes(item.Data.ToCharArray());
							gzipStream.Write(bytes, 0, bytes.Length);
						}
					}

					gzipStream.Close();
					gzipStream = null;
                    
                    // Send cached file to user
					this.StreamFromTo(File.OpenRead(Path.Combine(this.CachePath, key + ".gz")), to_stream, 4096, CloseMode.InStream);
				} 
                else 
                {
					gzipStream = new GZipStream(to_stream, CompressionMode.Compress);

					// Compress all files into output stream
					foreach (CompressItem item in items) 
                    {
						// Add file
						if (item.Type == CompressItemType.File) 
                            StreamFromTo(File.OpenRead(item.Data), gzipStream, 4096, CloseMode.InStream);

						// Add chunk
						if (item.Type == CompressItemType.Chunk) 
                        {
							bytes = Encoding.ASCII.GetBytes(item.Data.ToCharArray());
							gzipStream.Write(bytes, 0, bytes.Length);
						}
					}
				}
			} 
            finally 
            {
				if (gzipStream != null) gzipStream.Close();
			}
		}

		#region private

		private void SendPlainText(string key, Stream to_stream) 
        {
			Stream fileStream = null;
			byte[] bytes;

			// Check for cached file on disk, stream that one if it was found
			if (this.DiskCache && File.Exists(Path.Combine(this.CachePath, key + ".js"))) {
				this.StreamFromTo(File.OpenRead(Path.Combine(this.CachePath, key + ".js")), to_stream, 4096, CloseMode.InStream);
				return;
			}

			// Build new plain text stream
			if (this.DiskCache) 
            {
				try 
                {
					fileStream = File.OpenWrite(Path.Combine(this.CachePath, key + ".js"));

					// Compress all files into memory
					foreach (CompressItem item in items) 
                    {
						// Add file
						if (item.Type == CompressItemType.File)
							StreamFromTo(File.OpenRead(item.Data), fileStream, 4096, CloseMode.InStream);

						// Add chunk
						if (item.Type == CompressItemType.Chunk) {
							bytes = Encoding.ASCII.GetBytes((item.Data).ToCharArray());
							fileStream.Write(bytes, 0, bytes.Length);
						}
					}

					// Close file stream
					fileStream.Close();
					fileStream = null;
				} 
                finally 
                {
					// Close file stream
					if (fileStream != null)
						fileStream.Close();
				}

				// Send cached file to user
				this.StreamFromTo(File.OpenRead(Path.Combine(this.CachePath, key + ".js")), to_stream, 4096, CloseMode.InStream);
			} 
            else 
            {
				// Concat all files into output stream
				foreach (CompressItem item in items) 
                {
					// Add file
					if (item.Type == CompressItemType.File)
						StreamFromTo(File.OpenRead(item.Data), to_stream, 4096, CloseMode.InStream);

					// Add chunk
					if (item.Type == CompressItemType.Chunk) 
                    {
						bytes = Encoding.ASCII.GetBytes(item.Data.ToCharArray());
						to_stream.Write(bytes, 0, bytes.Length);
					}
				}
			}
		}

		private void StreamFromTo(Stream in_stream, Stream out_stream, int buff_size, CloseMode mode) 
        {
			byte[] buff = new byte[buff_size];
			int len;

			try 
            {
				while ((len = in_stream.Read(buff, 0, buff_size)) > 0) 
                {
					out_stream.Write(buff, 0, len);
					out_stream.Flush();
				}
			} 
            finally 
            {
				if (in_stream != null && (mode == CloseMode.Both || mode == CloseMode.InStream)) in_stream.Close();
				if (out_stream != null && (mode == CloseMode.Both || mode == CloseMode.OutStream)) out_stream.Close();
			}
		}

		private string MD5(string str) 
        {
			MD5 md5 = new MD5CryptoServiceProvider();
			byte[] result = md5.ComputeHash(Encoding.ASCII.GetBytes(str));
			str = BitConverter.ToString(result);

			return str.Replace("-", "");
		}

		#endregion
	}
}
